<?php
/**
 * (c) 2010 Chris Maciejewski
 * 
 * Permission is hereby granted, free of charge, to any person obtaining 
 * a copy of this software and associated documentation files 
 * (the "Software"), to deal in the Software without restriction, 
 * including without limitation the rights to use, copy, modify, merge,
 * publish, distribute, sublicense, and/or sell copies of the Software, 
 * and to permit persons to whom the Software is furnished to do so, 
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included 
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
 * DEALINGS IN THE SOFTWARE.
 * 
 */

/**
 * PHP Xpay class
 * 
 * @ingroup  API
 * @author Chris Maciejewski <chris@level7systems.co.uk>
 * 
 * @version    SVN: $Id: PhpXpay.class.php 13 2010-04-26 09:23:13Z level7systems $
 */
require_once 'PhpXpay.Exception.php';

class PhpXpay
{
  private $debug = false;
  private $xpay_host;
  private $xpay_port;
  private $socket;
  private $operation;
  private $site_id;
  
  private $request = array();
  private $response = array();
  
  /**
   * Constructor
   * 
   * @param $site_id
   * @param $xpay_host (default: 127.0.0.1)
   * @param $xpay_port (default: 5000)
   */
  public function __construct($site_id, $xpay_host = '127.0.0.1', $xpay_port = 5000)
  {
    error_reporting(E_ALL);
    
    $this->site_id = $site_id;
    $this->xpay_host = $xpay_host;
    $this->xpay_port = $xpay_port;
    
    $this->createSocket();
  }
  
  /**
   * Destructor
   */
  public function __destruct()
  {
    $this->closeSocket();
  }
  
  /**
   * Sets debuggin ON/OFF
   * 
   * @param bool $status
   */
  public function setDebug($status = false)
  {
    $this->debug = $status;
  }
  
  /**
   * Create network socket
   *
   * @throws PhpXpayException if failed to connect
   */
  private function createSocket()
  {
    $errno = 0;
    $errstr = '';
    
    if (!$this->socket = @fsockopen($this->xpay_host, $this->xpay_port, &$errno, &$errstr))
    {
      throw new PhpXpayException("Connection to ".$this->xpay_host.":".$this->xpay_port." failed. Error no: ".$errno ." - ". $errstr);
    }
  }
  
  /**
   * Close the connection
   *
   * @throws PhpXpayException if failed to close
   */
  private function closeSocket()
  {
    if (!fclose($this->socket))
    {
      throw new PhpXpayException("Failed to close socket.");
    }
  }
  
  /**
   * Sets amount
   * 
   * @param int $amount
   *
   * @throws PhpXpayException
   */
  public function setAmount($amount)
  { 
    if (!preg_match('/^[0-9]+\.{0,1}[0-9]{0,2}$/',$amount))
    {
      throw new PhpXpayException("Invalid amount $amount.");
    }
    
    $this->request['Operation']['Amount'] = $amount * 100;
  }
  
  /**
   * Sets currency
   * 
   * @param string $currency
   */
  public function setCurrency($currency)
  { 
    $this->request['Operation']['Currency'] = strtoupper($currency);
  }
  
  /**
   * Sets first name
   * 
   * @param string $first_name
   */
  public function setFirstName($first_name)
  { 
    $this->request['CustomerInfo']['Postal']['Name']['FirstName'] = $first_name;
  }
  
  /**
   * Sets last name
   * 
   * @param string $last_name
   */
  public function setLastName($last_name)
  { 
    $this->request['CustomerInfo']['Postal']['Name']['LastName'] = $last_name;
  }
  
  /**
   * Sets company name
   * 
   * @param string $company
   */
  public function setCompany($company)
  { 
    $this->request['CustomerInfo']['Postal']['Company'] = $company;
  }
  
  /**
   * Sets street
   * 
   * @param string $street
   */
  public function setStreet($street)
  { 
    $this->request['CustomerInfo']['Postal']['Street'] = $street;
  }
  
  /**
   * Sets city
   * 
   * @param string $city
   */
  public function setCity($city)
  { 
    $this->request['CustomerInfo']['Postal']['City'] = $city;
  }
  
  /**
   * Sets state
   * 
   * @param string $state
   */
  public function setState($state)
  { 
    $this->request['CustomerInfo']['Postal']['StateProv'] = $state;
  }
  
  /**
   * Sets post code
   * 
   * @param string $post_code
   */
  public function setPostCode($post_code)
  { 
    $this->request['CustomerInfo']['Postal']['PostalCode'] = $post_code;
  }
  
  /**
   * Sets country
   * 
   * @param string $country
   */
  public function setCountry($country)
  { 
    $this->request['CustomerInfo']['Postal']['CountryCode'] = $country;
  }
  
  /**
   * Sets phone
   * 
   * @param string $phone
   */
  public function setPhone($phone)
  { 
    $this->request['CustomerInfo']['Telecom']['Phone'] = $phone;
  }
  
  /**
   * Sets email
   * 
   * @param string $email
   */
  public function setEmail($email)
  { 
    $this->request['CustomerInfo']['Online']['Email'] = $email;
  }
  
  /**
   * Sets card type
   * 
   * @param string $card_type
   */
  public function setCardType($card_type)
  { 
    $this->request['PaymentMethod']['CreditCard']['Type'] = $card_type;
  }
  
  /**
   * Sets card number
   * 
   * @param string $card_no
   */
  public function setCardNo($card_no)
  { 
    $this->request['PaymentMethod']['CreditCard']['Number'] = str_replace(" ","",$card_no);
  }
  
  /**
   * Sets expiry date
   * 
   * @param string $expiry_date
   */
  public function setExpiryDate($expiry_date)
  { 
    if (!preg_match('/^[0-9]{2}\/[0-9]{2}$/',$expiry_date))
    {
      throw new PhpXpayException ("Invalid expiry date.");
    }
    
    $this->request['PaymentMethod']['CreditCard']['ExpiryDate'] = $expiry_date;
  }
  
  /**
   * Sets card security code
   * 
   * @param int $security_code
   */
  public function setSecurityCode($security_code)
  { 
    $this->request['PaymentMethod']['CreditCard']['SecurityCode'] = $security_code;
  }
  
  /**
   * Sets Parent Transaction Reference (for Reversal operation)
   * 
   * @param int $parent_ref
   */
  public function setParentTransactionReference($parent_ref)
  { 
    $this->request['PaymentMethod']['CreditCard']['ParentTransactionReference'] = $parent_ref;
  }
  
  /**
   * Sets Transaction Verifier (for Reversal operation)
   * 
   * @param int $parent_ref
   */
  public function setTransactionVerifier($transaction_verifier)
  { 
    $this->request['PaymentMethod']['CreditCard']['TransactionVerifier'] = $transaction_verifier;
  }
  
  /**
   * Sets order reference
   * 
   * @param string $order_ref
   */
  public function setOrderRef($order_ref)
  { 
    $this->request['Order']['OrderReference'] = $order_ref;
  }
  
  /**
   * Sets order info
   * 
   * @param string $order_info
   */
  public function setOrderInfo($order_info)
  { 
    $this->request['Order']['OrderInformation'] = $order_info;
  }
  
  /**
   * Sets Transaction Reference
   * 
   * @param string $transaction_reference
   */
  public function setTransactionReference($transaction_reference)
  { 
    $this->request['Operation']['TransactionReference'] = $transaction_reference;
  }
  
  /**
   * Sets Settle Date
   * 
   * @param string $settle_date
   */
  public function setSettleDate($settle_date)
  { 
    $this->request['Operation']['SettleDate'] = $settle_date;
  }
  
  /**
   * Sets Settle Status
   * 
   * @param string $settle_status
   */
  public function setSettleStatus($settle_status)
  { 
    $this->request['Operation']['SettleStatus'] = $settle_status;
  }
  
  /**
   * Sends autrhoization request
   */
  public function doAuthorize()
  {
    $this->operation = 'AUTH';
    $this->request['Operation']['SettlementDay'] = 0;
    
    return $this->sendRequest();
  }
  
  /**
   * Sends Charge request
   */
  public function doCharge()
  {
    $this->operation = 'AUTH';
    $this->request['Operation']['SettlementDay'] = 1;
    
    return $this->sendRequest();
  }
  
  /**
   * Confirm of pre-authorization
   */
  public function doConfirm()
  {
    $this->operation = 'SETTLEMENT';
    
    if (!isset($this->request['Operation']['SettleDate']))
    {
      $this->request['Operation']['SettleDate'] = 'NEXT';
    }
    
    if (!isset($this->request['Operation']['SettleStatus']))
    {
      $this->request['Operation']['SettleStatus'] = 1;
    }
    
    return $this->sendRequest();
  }
  
  /**
   * Void of pre-authorization (reversal)
   */
  public function doVoid()
  {
    $this->operation = 'AUTHREVERSAL';
    
    return $this->sendRequest();
  }
  
  /**
   * Refunds previously settled transaction
   */
  public function doRefund()
  {
    $this->operation = 'REFUND';
    
    return $this->sendRequest();
  }
  
  /**
   * Query status of transaction
   */
  public function doQuery()
  {
    $this->operation = 'TRANSACTIONQUERY';
    
    return $this->sendRequest();
  }
  
  /**
   * Sends data and returns response
   * 
   * @param string $data
   * @throws PhpXpayException
   * @return array
   */
  private function sendRequest()
  {
    $xml = $this->formatXML();
    
    if ($this->debug)
    {
      echo "RX -->\n".$xml."\n";
    }
    
    if (!fputs($this->socket, $xml, strlen($xml)))
    {
      throw new PhpXpayException ("Failed to write to socket");
    }

    $data = '';

    while (!feof($this->socket))
    {
      $chunk = fgets($this->socket,4096);

      $data.= $chunk;

      if (trim($chunk) == "</ResponseBlock>")
      {
        break;
      }
    }
    
    if ($this->debug)
    { 
      echo "<-- TX\n".$data."\n";
    }
    
    if (!$xml = @simplexml_load_string($data))
    {
      throw new PhpXpayException("Failed to read response.");
    }
    
    $this->response = $this->object2array($xml);
    
    return $this->processResponse();
  }
  

  // helper XML writer
  private function _write(XMLWriter $xml, $data)
  {
    foreach($data as $key => $value)
    {
      if (is_array($value))
      {
        $xml->startElement($key);
        $this->_write($xml, $value);
        $xml->endElement();
        continue;
      }
      
      $xml->writeElement($key, utf8_decode($value));
    }
  }
  
  /**
   * Gets current request data
   * 
   * @return array
   */
  public function getRequest()
  {
    return $this->request;
  }
  
  /**
   * Formats XML request
   * 
   * @return string
   */
  private function formatXML()
  {
    $this->request['Operation']['SiteReference'] = $this->site_id;
    
    $xml = new XmlWriter();
    $xml->openMemory();
    
    if ($this->debug)
    {
      $xml->setIndent(true);
      $xml->setIndentString('  ');
    }
    
    $xml->startDocument('1.0', 'iso-8859-1');
    $xml->startElement('RequestBlock');
    $xml->writeAttribute('Version','3.51');
    $xml->startElement('Request');
    $xml->writeAttribute('Type',$this->operation);
    $this->_write($xml, $this->request);
    $xml->endElement(); // Request
    $xml->writeElement('Certificate',$this->site_id);
    $xml->endElement(); // RequestBlock
    
    return $xml->outputMemory()."\n";
  }
  
  /**
   * Process response
   * 
   * @param array $response
   */
  private function processResponse()
  {
    
    if (!$this->response || !is_array($this->response))
    {
      return array(
        "result"    => false,
        "info"      => "No response from payment gateway.",
        "request"   => serialize($this->request),
        "response"  => "NA"
      );
    }
    
    if (!isset($this->response['Response']) || !isset($this->response['Response']['OperationResponse']))
    {
      return array(
        "result"    => false,
        "result"    => "Failed to parse response.",
        "request"   => serialize($this->request),
        "response"  => serialize($this->response)
      );
    }
    
    $res = $this->response['Response']['OperationResponse'];
    
    if (isset($res['Result']) && $res['Result'] == "1")
    {
      if (!isset($res['TransactionReference']))
      {
        return array(
          "result"    => false,
          "result"    => "Result = 1, but no TransactionReference found.",
          "request"   => serialize($this->request),
          "response"  => serialize($this->response)
        );
      }
      
      if ($this->operation == 'AUTH' && !isset($res['TransactionVerifier']))
      {
        return array(
          "result"    => false,
          "result"    => "Result = 1, operation = AUTH, but no TransactionVerifier found.",
          "request"   => serialize($this->request),
          "response"  => serialize($this->response)
        );
      }
      
      $gw_id = (isset($res['TransactionVerifier'])) ? $res['TransactionReference']."|".$res['TransactionVerifier'] : $res['TransactionReference'];
      
      return array(
        "result"    => true,
        "gw_id"     => $gw_id,
        "request"   => serialize($this->request),
        "response"  => serialize($this->response)
      );
    }
    else
    {
      $info = (isset($res['Message'])) ? $res['Message'] : 'NA';
      
      return array(
        "result"    => false,
        "info"      => $info,
        "request"   => serialize($this->request),
        "response"  => serialize($this->response)
      );
    }
  }
  
  /**
   * Converts object to an array
   * 
   * @param mixed $boject
   * 
   * @return array or null
   */
  private function object2array($object)
  {
    $return = NULL;
    
    if (is_array($object))
    {
      foreach($object as $key => $value)
      {
        $return[$key] = $this->object2array($value);
      }
    }
    else
    {
      $var = get_object_vars($object);
        
      if ($var)
      {
        foreach($var as $key => $value)
        {
          $return[$key] = ($key && !$value) ? NULL : $this->object2array($value);
        }
      }
      else
      {
        return $object;
      }
    }
    
    return $return;
  }
}

?>
